/*
 * (C) Copyright 2006-2010 Nuxeo SAS (http://nuxeo.com/) and contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl.html
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * Contributors:
 *     bstefanescu
 */
package org.nuxeo.gwt.habyt.app.client.js;

import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsonUtils;

/**
 * Represents a JSON object. A JSON object consists of a set of properties.
 */
public class JsObject {

    private final JavaScriptObject jsObject;

    public JsObject() {
        this(JavaScriptObject.createObject());
    }

    /**
     * Creates a new JSONObject from the supplied JavaScript value.
     */
    public JsObject(JavaScriptObject jsValue) {
        jsObject = jsValue;
    }

    /**
     * Tests whether or not this JSONObject contains the specified property.
     * 
     * @param key the property to search for
     * @return <code>true</code> if the JSONObject contains the specified
     *         property
     */
    public native boolean containsKey(String key) /*-{
                                                  return key in this.@org.nuxeo.gwt.habyt.app.client.js.JsObject::jsObject;
                                                  }-*/;

    /**
     * Returns <code>true</code> if <code>other</code> is a {@link JsObject}
     * wrapping the same underlying object.
     */
    @Override
    public boolean equals(Object other) {
        if (!(other instanceof JsObject)) {
            return false;
        }
        return jsObject.equals(((JsObject) other).jsObject);
    }

    /**
     * Gets the JSONValue associated with the specified property.
     * 
     * @param key the property to access
     * @return the value of the specified property, or <code>null</code> if the
     *         property does not exist
     * @throws NullPointerException if key is <code>null</code>
     */
    public String get(String key) {
        if (key == null) {
            throw new NullPointerException();
        }
        return get0(key);
    }

    /**
     * Returns the underlying JavaScript object that this object wraps.
     */
    public JavaScriptObject getJavaScriptObject() {
        return jsObject;
    }

    @Override
    public int hashCode() {
        return jsObject.hashCode();
    }

    /**
     * Returns the set of properties defined on this JSONObject. The returned
     * set is immutable.
     */
    public Set<String> keySet() {
        final String[] keys = computeKeys();
        return new AbstractSet<String>() {
            @Override
            public boolean contains(Object o) {
                return (o instanceof String) && containsKey((String) o);
            }

            @Override
            public Iterator<String> iterator() {
                return Arrays.asList(keys).iterator();
            }

            @Override
            public int size() {
                return keys.length;
            }
        };
    }

    /**
     * Assign the specified property to the specified value in this JSONObject.
     * If the property already has an associated value, it is overwritten.
     * 
     * @param key the property to assign
     * @param value the value to assign
     * @return the previous value of the property, or <code>null</code> if the
     *         property did not exist
     * @throws NullPointerException if key is <code>null</code>
     */
    public String put(String key, String value) {
        if (key == null) {
            throw new NullPointerException();
        }
        String previous = get(key);
        put0(key, value);
        return previous;
    }

    /**
     * Determines the number of properties on this object.
     */
    public int size() {
        // Must always recheck due to foreign changes. :(
        return computeSize();
    }

    /**
     * Converts a JSONObject into a JSON representation that can be used to
     * communicate with a JSON service.
     * 
     * @return a JSON string representation of this JSONObject instance
     */
    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer();
        sb.append("{");
        boolean first = true;
        String[] keys = computeKeys();
        for (String key : keys) {
            if (first) {
                first = false;
            } else {
                sb.append(", ");
            }
            sb.append(JsonUtils.escapeValue(key));
            sb.append(":");
            sb.append(get(key));
        }
        sb.append("}");
        return sb.toString();
    }

    private native void addAllKeys(Collection<String> s) /*-{
                                                         var jsObject = this.@org.nuxeo.gwt.habyt.app.client.js.JsObject::jsObject;
                                                         for (var key in jsObject) {
                                                         if (jsObject.hasOwnProperty(key)) {
                                                         s.@java.util.Collection::add(Ljava/lang/Object;)(key);
                                                         } 
                                                         }
                                                         }-*/;

    private String[] computeKeys() {
        if (GWT.isScript()) {
            return computeKeys0(new String[0]);
        } else {
            List<String> result = new ArrayList<String>();
            addAllKeys(result);
            return result.toArray(new String[result.size()]);
        }
    }

    private native String[] computeKeys0(String[] result) /*-{
                                                          var jsObject = this.@org.nuxeo.gwt.habyt.app.client.js.JsObject::jsObject;
                                                          var i = 0;
                                                          for (var key in jsObject) {
                                                          if (jsObject.hasOwnProperty(key)) {
                                                          result[i++] = key;
                                                          }
                                                          }
                                                          return result;
                                                          }-*/;

    private native int computeSize() /*-{
                                     var jsObject = this.@org.nuxeo.gwt.habyt.app.client.js.JsObject::jsObject;
                                     var size = 0;
                                     for (var key in jsObject) {
                                     if (jsObject.hasOwnProperty(key)) {
                                     ++size;
                                     }
                                     }
                                     return size;
                                     }-*/;

    private native String get0(String key) /*-{
                                           var jsObject = this.@org.nuxeo.gwt.habyt.app.client.js.JsObject::jsObject;
                                           var v;
                                           // In Firefox, jsObject.hasOwnProperty(key) requires a primitive string
                                           key = String(key);       
                                           if (jsObject.hasOwnProperty(key)) {
                                           v = jsObject[key];
                                           }
                                           return v;
                                           }-*/;

    private native void put0(String key, String value) /*-{
                                                       if (value) {
                                                       this.@org.nuxeo.gwt.habyt.app.client.js.JsObject::jsObject[key] = value;
                                                       } else {
                                                       delete this.@org.nuxeo.gwt.habyt.app.client.js.JsObject::jsObject[key];
                                                       }
                                                       }-*/;

}
